// License Notification
//======================================================
// Copyright (c) 2010 Channel Intelligence Inc.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// ======================================================




using System;
using System.IO;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using CIP4.Item;
using CIP4.Common;

namespace CIP4.Serializer
{

    public class WrapperSerializerType : ISerializerType, IDisposable
    {
        #region Properties

        private CIP4ItemAttribute _itemAttribute;
        public CIP4ItemAttribute ItemAttribute
        {
            get
            {
                if (_itemAttribute == null)
                {
                    CIP4ItemAttribute att = CIP4ItemAttribute.GetAttribute(TargetType);
                    if (string.IsNullOrEmpty(att.ActualName))
                        att.ActualName = TargetType.Name;
                    return att;
                }
                return _itemAttribute;
            }
            private set
            {
                _itemAttribute = value;
                if (string.IsNullOrEmpty(_itemAttribute.ActualName))
                    _itemAttribute.ActualName = TargetType.Name;
            }
        }

        public Type TargetType { get; private set; }

        public Type SerializerType 
        {
            get
            {
                if (SerializeInfo != null)
                    return SerializeInfo.DeclaringType;
                return null;
            }
        }

        private readonly List<LoggerDelegate> _loggerDelegates = new List<LoggerDelegate>();

        public LoggerDelegate Logger 
        { 
            get 
            { 
                return _loggerDelegates.Count == 0 ? Globals.DefaultLogger : _loggerDelegates[_loggerDelegates.Count - 1]; 
            } 
        }

        public Dictionary<string, KeyValuePair<PropertyInfo, object>> StreamSeeds { get; set; }

        public void PushLogger(LoggerDelegate logger)
        {
            _loggerDelegates.Add(logger);
        }

        public void PopLogger()
        {
            if (_loggerDelegates.Count > 0)
                _loggerDelegates.RemoveAt(_loggerDelegates.Count - 1);
        }

        public void SetLogger(LoggerDelegate logger)
        {
            if (_loggerDelegates.Count == 0)
                PushLogger(logger);
            else
                _loggerDelegates[_loggerDelegates.Count - 1] = logger;
        }

        public MethodInfo SerializeInfo { get; private set; }

        public MethodInfo DeSerializeInfo { get; private set; }

        public MethodInfo GetExternalIDInfo { get; private set; }

        public MethodInfo GetValueForPathInfo { get; private set; }

        public MethodInfo GetKeysInfo { get; private set; }

        public MethodInfo GetVersionNumberInfo { get; private set; }

        public MethodInfo SetVersionNumberInfo { get; private set; }

        public MethodInfo GetItemVersionInfo { get; private set; }

        private Assembly TargetAssembly { get; set; }

        #endregion

        #region public

        public WrapperSerializerType(SerializerKindEnum serializerKind, Type targetType, string serializerLocation, CIP4ItemAttribute itemAttribute, LoggerDelegate logger)
        {
            StreamSeeds = new Dictionary<string, KeyValuePair<PropertyInfo, object>>();
            PushLogger(logger);
            TargetType = targetType;
            ItemAttribute = itemAttribute;
            SetupSpecific(serializerKind, serializerLocation);
        }

        /// <summary>
        /// This is for the case where the serializer handles a generic type - there are two types involved
        /// 1. The host type 
        /// 2. the target type
        /// Something like 
        ///     hostType&lt;aType&gt; 
        /// and
        ///     hostType&lt;targeType&gt; 
        ///     
        /// A serializer will have been built that contains a specially generated serializer that can handle the 
        /// combination of hostType and targetType. That serializer is what is passed in to this constructor. The constructor
        /// builds this serializer by finding the appropriate serializer type in the assembly that hosts the passed in serializer
        /// </summary>
        /// <param name="targetType"></param>
        /// <param name="hostType"></param>
        /// <param name="otherSerializer"></param>
        public WrapperSerializerType(Type targetType, Type hostType, ISerializerType otherSerializer)
        {
            Type anOriginal = hostType.MakeGenericType(targetType);
            TargetType = anOriginal;
            TypeContextType tc = new TypeContextType { OriginalType = anOriginal };
            Type targetSerializer = otherSerializer.SerializerType.Assembly.GetType("CIP4.Serializer." + tc.TypeSerializerName);
            if (targetSerializer != null)
                FindMethods(targetSerializer, otherSerializer.SerializerType.Assembly.Location);
            else
                Logger(MajorEnum.Error, MinorEnum.CannotFndSerializerType,
                    "Cannot find the serializer for the type {0} in assembly {1}",
                    new object[] { targetType.Name, otherSerializer.SerializerType.Assembly.Location });
        }

        public object GetDataForObject(object forObject)
        {
            if (SerializeInfo == null)
                throw new ArgumentException("The serializer has not been bound correctly - look for serializer compile issues or anything that might prevet access to the serializer");
            StreamSeeds = new Dictionary<string, KeyValuePair<PropertyInfo, object>>();
            return SerializeInfo.Invoke(null, new[] { forObject, StreamSeeds, this});
        }

        public object GetObjectForData(object data)
        {
            if (DeSerializeInfo == null)
                throw new ArgumentException("The serializer has not been bound correctly - look for serializer compile issues or anything that might prevet access to the serializer");
            StreamSeeds = new Dictionary<string, KeyValuePair<PropertyInfo, object>>();
            return DeSerializeInfo.Invoke(null, new[] { DateTime.UtcNow, data, StreamSeeds, string.Empty });
        }

        public ExternalIDType GetExternalID(object data)
        {
            if (GetExternalIDInfo == null)
                return default(ExternalIDType);
            return (ExternalIDType)GetExternalIDInfo.Invoke(null, new[] { data });
        }

        public IEnumerable<KeyValuePair<string, int>> GetKeys()
        {
            if (GetKeysInfo == null)
                return new List<KeyValuePair<string, int>>();
            return (IEnumerable<KeyValuePair<string, int>>)GetKeysInfo.Invoke(null, new object[] { });
        }

        public bool CheckVersion { get; set; }

        public Int64 GetVersionNumber(object item)
        {
            if (GetVersionNumberInfo == null)
                return 0;
            return (Int64)GetVersionNumberInfo.Invoke(null, new[] { item });
        }

        public void SetVersionNumber(object item, Int64 value)
        {
            if (SetVersionNumberInfo == null)
                return;
            SetVersionNumberInfo.Invoke(null, new[] { item, value });
        }

        public object GetValueForSimpleValue(object valueObject, string typeName, OtherConverterDelegate otherConverter)
        {
            throw new NotImplementedException();
        }

        public object GetValueForPath(object item, string path)
        {
            string[] parts = path.Split('.');
            if (path == "ExternalID" && parts.Length == 1)
                return GetValueForPathInfo.Invoke(null, new[] { parts, 0, item }).ToString();
            if (string.Compare(parts[0], "item", true) == 0 || parts[0] == item.GetType().Name)
                return GetValueForPathInfo.Invoke(null, new[] { parts, 1, item });

            throw new ArgumentException(string.Format("Cannot get vakue - first token in the path is not recognized: {0}", path));
        }

        public object GetItemVersion(long versionTimestamp, object previousVersion, Dictionary<string, object> values)
        {
            if (GetItemVersionInfo == null)
                return null;
            return GetItemVersionInfo.Invoke(null, new[] { versionTimestamp, previousVersion, values });
        }

        #endregion

        #region internal, protected and private

        private void SetupSpecific(SerializerKindEnum serializerKind, string serializerLocation)
        {
            string serializerName = Path.GetFileNameWithoutExtension(TargetType.Assembly.Location) + string.Format("_{0}Serializer.dll", serializerKind);

#if LOADSERIALIZERTRACE
            Logger(MajorEnum.Trace, MinorEnum.LoadingSerializer, "Current thread {0}: First AssemblyGet call for {2} for the type {1}", new object[] { Thread.CurrentThread.ManagedThreadId, ItemAttribute.ActualName, serializerName });
#endif
            // Could be someone has already added it to the cache - avoid going  to the file system 
            // by looking first of all in the cache to see if it's there yet. Don't use AssemblyResolve 
            // as that would add it to the cache (regardless of timestamps) if it's not already there.
            TargetAssembly = Globals.AssemblyGet(Path.GetFileNameWithoutExtension(serializerName));

            if (TargetAssembly == null)
            {

                string mutexNamePrefix = "Global\\" + CIP4Versions.CIPlVersionDomain + ".";

                if (serializerName.Length + mutexNamePrefix.Length > 260)
                    throw new ArgumentException(
                        string.Format("The dll name ({0}) is too long - it must be less than {1} characters",
                        serializerName, 260 - mutexNamePrefix.Length));

                FileInfo fi;

                Mutex serializerDllMutex = null;

                try
                {
                    serializerDllMutex = new Mutex(false, mutexNamePrefix + serializerName);

                    if (!serializerDllMutex.WaitOne(new TimeSpan(0, 0, 10)))
                    {
                        throw new ApplicationException(String.Format("Current thread {0}: Failed to get the mutex guarding access to the {1} serializer", Thread.CurrentThread.ManagedThreadId, serializerName));
                    }
#if LOADSERIALIZERTRACE
                    else
                    {
                        Logger(MajorEnum.Trace, MinorEnum.SerializerBuildMutexTrace, "Current thread {0}: Acquired the mutex - loading serializer {1}", new object[] { Thread.CurrentThread.ManagedThreadId, serializerName });
                    }
#endif
                    // Could be some thread ahead of this one on the mutex has built the assembly and added it to the cache 
                    // - avoid going to the file system by looking first of all in the cache to see if it's there yet. Don't 
                    // use AssemblyResolve as that would add it to the cache (regardless of timestamps) if it's not already there.
                    TargetAssembly = Globals.AssemblyGet(Path.GetFileNameWithoutExtension(serializerName));


                    if (TargetAssembly == null)
                    {
                        fi = new FileInfo(Path.Combine(serializerLocation, serializerName));
#if LOADSERIALIZERTRACE

                        Logger(MajorEnum.Trace, MinorEnum.LoadingSerializer, "Current thread {0}: Loading serializer assembly {2} for the type {1}", new object[] { Thread.CurrentThread.ManagedThreadId, ItemAttribute.ActualName, fi.Name });
#endif
                        if (!fi.Exists)
                        {
                            Logger(MajorEnum.Warning, MinorEnum.SpontaneousSerializer, "Current thread {0}: Unable to find serializer for {1} - attempting to build one - watch for fireworks", new object[] { Thread.CurrentThread.ManagedThreadId, TargetType.Assembly.Location });
                            GeneratorBuilderType.GenerateAndBuild(serializerKind, TargetType.Assembly.Location, serializerLocation, new List<string>(), Logger);
                            fi.Refresh();
                        }
                        else if (fi.LastWriteTimeUtc < new FileInfo(TargetType.Assembly.Location).LastWriteTimeUtc)
                        {
                            Logger(MajorEnum.Warning, MinorEnum.SpontaneousSerializer, "Current thread {0}: Serializer {1} for {2} seems to be out of date - attempting to build a new one - watch for fireworks", new object[] { Thread.CurrentThread.ManagedThreadId, fi.FullName, TargetType.Assembly.Location });
                            GeneratorBuilderType.GenerateAndBuild(serializerKind, TargetType.Assembly.Location, serializerLocation, new List<string>(), Logger);
                        }

                        if (!fi.Exists)
                        {
                            Logger(MajorEnum.Error, MinorEnum.CannotFndSerializerAssembly,
                                "Current thread {0}: Cannot find the serializer assembly {2} for the type {1}",
                                new object[] { Thread.CurrentThread.ManagedThreadId, ItemAttribute.ActualName, fi.FullName });
                            return;
                        }

                        TargetAssembly = Globals.AssemblyResolve(Path.GetFileNameWithoutExtension(fi.Name), serializerLocation);
                    }
#if LOADSERIALIZERTRACE
                    else
                    {
                        Logger(MajorEnum.Trace, MinorEnum.LoadingSerializer, "Current thread {0}: Second AssemblyGet call succeeded for {1}", new object[] { Thread.CurrentThread.ManagedThreadId, serializerName });
                    }
#endif
                }
                finally
                {
                    if (serializerDllMutex != null)
                    {
                        serializerDllMutex.ReleaseMutex();
                        serializerDllMutex.Close();
                    }
                }
            }
#if LOADSERIALIZERTRACE
            else
            {
                Logger(MajorEnum.Trace, MinorEnum.LoadingSerializer, "Current thread {0}: First AssemblyGet call succeeded for {1}", new object[] { Thread.CurrentThread.ManagedThreadId, serializerName });
            }
#endif
            if (TargetAssembly == null)
            {
                Logger(MajorEnum.Error, MinorEnum.CannotLoadSerializerAssembly,
                    "Current thread {0}: Cannot load the serializer assembly {2} for the type {01}",
                    new object[] { Thread.CurrentThread.ManagedThreadId, ItemAttribute.ActualName, Path.Combine(serializerLocation, serializerName) });
                return;
            }
            Type serializerType = TargetAssembly.GetType("CIP4.Serializer." + ItemAttribute.ActualName + "_Serializer");

            if (serializerType == null)
            {
                Logger(MajorEnum.Error, MinorEnum.CannotFndSerializerType,
                    "Current thread {0}: Cannot find the serializer for the type {1} in assembly {2}",
                    new object[] { Thread.CurrentThread.ManagedThreadId, ItemAttribute.ActualName, Path.Combine(serializerLocation, serializerName) });
                return;
            }

            FindMethods(serializerType, Path.Combine(serializerLocation, serializerName));
#if LOADSERIALIZERTRACE

            Logger(MajorEnum.Trace, MinorEnum.LoadingSerializer, "Current thread {0}: Serializer loaded {2} for the type {1}", new object[] { Thread.CurrentThread.ManagedThreadId, ItemAttribute.ActualName, serializerName });
#endif
        }

        private void FindMethods(Type serializerType, string assemblyLocation)
        {
            foreach (MethodInfo m in serializerType.GetMethods())
            {
                if (m.Name == "SerializeItem" && m.GetParameters().Length == 3)
                    SerializeInfo = m;
                else if (m.Name == "DeSerialize" && m.GetParameters().Length == 4)
                    DeSerializeInfo = m;
                else if (m.Name == "GetExternalIDForObject")
                    GetExternalIDInfo = m;
                else if (m.Name == "GetValueForPath")
                    GetValueForPathInfo = m;
                else if (m.Name == "GetKeys")
                    GetKeysInfo = m;
                else if (m.Name == "GetVersionNumber")
                    GetVersionNumberInfo = m;
                else if (m.Name == "SetVersionNumber")
                    SetVersionNumberInfo = m;
                else if (m.Name == "GetItemVersion")
                    GetItemVersionInfo = m;
                //
            }

            if (SerializeInfo == null)
            {
                Logger(MajorEnum.Error, MinorEnum.CannotFindSerializerFunction,
                    "Cannot find the serializer function for the type {0} in assembly {1}",
                    new object[] { ItemAttribute.ActualName, assemblyLocation });
                return;
            }

            if (DeSerializeInfo == null)
            {
                Logger(MajorEnum.Error, MinorEnum.CannotFindDeSerializerFunction,
                    "Cannot find the deserializer function for the type {0} in assembly {1}",
                    new object[] { ItemAttribute.ActualName, assemblyLocation });
                return;
            }
        }

        public static char ConvertToChar(string value)
        {
            if (value.StartsWith("0x"))
                return (char)Int16.Parse(value.Substring(2), System.Globalization.NumberStyles.HexNumber);
            if (value.StartsWith("#"))
                return (char)Int16.Parse(value.Substring(1), System.Globalization.NumberStyles.HexNumber);
            if (value.Length == 2)
                return (char)Int16.Parse(value, System.Globalization.NumberStyles.HexNumber);
            return value[0];
        }

        internal static SortedList<string, PropertyInfo> SortedProperties(PropertyInfo[] properties)
        {
            SortedList<string, PropertyInfo> list = new SortedList<string, PropertyInfo>();
            foreach (PropertyInfo prop in properties)
            {
                CIP4PropertyAttribute attribute = CIP4PropertyAttribute.GetAttribute(prop);
                list.Add((attribute != null && attribute.Ordinal > 0 ? attribute.Ordinal.ToString("0000") : string.Empty) + prop.Name, prop);
            }
            return list;
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
        }

        #endregion
    }

}
